/*  ----------------------------------------------------------------------------
 *  Arello Mobile
 *  Fast 3d Engine
 *  ----------------------------------------------------------------------------
 * Except where otherwise noted, this work is licensed under
 * http://creativecommons.org/licenses/by/3.0
 */

#include "Hardware.h"
#include <WindowImpl.h>

#include <Gluck.h>
#include <logs/Log.h>

#include "VertexBuffer.h"
#include "IndexBuffer.h"
#include "PointBuffer.h"
#include "Texture.h"

#include <system/assert.h>


namespace liba
{
namespace lib3d
{
namespace hard
{
namespace macx
{

class MatrixGL
{
	GLfloat data[16];
public:
	MatrixGL( const Matrix & mat )
	{
		for( int y = 0; y < 4; ++y )
			for( int x = 0; x < 4; ++x )
				data[ x*4 + y ] = mat.cr( x, y );		
	}
	void glLoad()const
	{
		glLoadMatrixf( data );
	}
	void glMult()const
	{
		glMultMatrixf( data );
	}	
};

Hardware::Hardware()
:   context( 0 ),
	window(0),
	viewRenderbuffer(0),
	viewFramebuffer(0),
	depthRenderbuffer(0),
	current_blend_mode(BM_UNINITIALIZED)
{
}

Hardware::~Hardware()
{
	stop_mode();
}
	
void Hardware::set_blend_mode(BlendMode bm)
{
	if (current_blend_mode == bm)
		return;
	
	switch (bm)
	{
		case BM_ALPHA:
			glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
			break;
		case BM_ADD:
			glBlendFunc(GL_ONE, GL_ONE);
			break;
		case BM_ADD_ALPHA:
			glBlendFunc(GL_SRC_ALPHA, GL_ONE);
			break;
		default:
			LIBA_FAIL("Unexpected blend mode");
	}
}
	
bool Hardware::set_mode_internal(WindowImpl * window, const Strings & attribute_values)
{
	this->window = window;
	create_framebuffer();
	
/*
	CAEAGLLayer * eaglLayer = (CAEAGLLayer*)window->get_layer();
	
	// Configure it so that it is opaque, does not retain the contents of the backbuffer when displayed, and uses RGBA8888 color.
	eaglLayer.opaque = YES;
	eaglLayer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys:
									[NSNumber numberWithBool:FALSE], kEAGLDrawablePropertyRetainedBacking,
									kEAGLColorFormatRGBA8, kEAGLDrawablePropertyColorFormat,
									nil];
	
	// Create our EAGLContext, and if successful make it current and create our framebuffer.
	context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES1];
	if(!context || ![EAGLContext setCurrentContext:(EAGLContext *)context] || !create_framebuffer())
	{
		return false;
	}
*/	
	return true;
}
	
void Hardware::resize_view()
{
//	[EAGLContext setCurrentContext:(EAGLContext *)context];
	destroy_framebuffer();
	create_framebuffer();
}

void Hardware::stop_mode()
{
	destroy_framebuffer();
	
/*
	if([EAGLContext currentContext] == context)
	{
		[EAGLContext setCurrentContext:nil];
	}
	
	[(EAGLContext* )context release];
	context = 0;
*/
}
	
bool Hardware::create_framebuffer()
{
	height = window->height;
	width = window->width;

	// Generate IDs for a framebuffer object and a color renderbuffer
/*
	glGenFramebuffersOES(1, &viewFramebuffer);
	glGenRenderbuffersOES(1, &viewRenderbuffer);
	
	glBindFramebufferOES(GL_FRAMEBUFFER_OES, viewFramebuffer);
	glBindRenderbufferOES(GL_RENDERBUFFER_OES, viewRenderbuffer);
	// This call associates the storage for the current render buffer with the EAGLDrawable (our CAEAGLLayer)
	// allowing us to draw into a buffer that will later be rendered to screen whereever the layer is (which corresponds with our view).
	[(EAGLContext *)context renderbufferStorage:GL_RENDERBUFFER_OES fromDrawable:(id<EAGLDrawable>)window->get_layer()];
	glFramebufferRenderbufferOES(GL_FRAMEBUFFER_OES, GL_COLOR_ATTACHMENT0_OES, GL_RENDERBUFFER_OES, viewRenderbuffer);
	
	glGetRenderbufferParameterivOES(GL_RENDERBUFFER_OES, GL_RENDERBUFFER_WIDTH_OES, &width);
	glGetRenderbufferParameterivOES(GL_RENDERBUFFER_OES, GL_RENDERBUFFER_HEIGHT_OES, &height);

	if(multisampleSupported)
	{
#ifndef IPAD_VERSION //3.2 does not have multisampling yet
		//Generate our MSAA Frame and Render buffers
		glGenFramebuffersOES(1, &msaaFramebuffer);
		glGenRenderbuffersOES(1, &msaaRenderBuffer);
		
		//Bind our MSAA buffers
		glBindFramebufferOES(GL_FRAMEBUFFER_OES, msaaFramebuffer);
		glBindRenderbufferOES(GL_RENDERBUFFER_OES, msaaRenderBuffer);
		
		// Generate the msaaDepthBuffer.
		// 4 will be the number of pixels that the MSAA buffer will use in order to make one pixel on the render buffer.
		glRenderbufferStorageMultisampleAPPLE(GL_RENDERBUFFER_OES, 4, GL_RGB5_A1_OES, width, height);
		glFramebufferRenderbufferOES(GL_FRAMEBUFFER_OES, GL_COLOR_ATTACHMENT0_OES, GL_RENDERBUFFER_OES, msaaRenderBuffer);
		glGenRenderbuffersOES(1, &msaaDepthBuffer);
		
		//Bind the msaa depth buffer.
		glBindRenderbufferOES(GL_RENDERBUFFER_OES, msaaDepthBuffer);
		glRenderbufferStorageMultisampleAPPLE(GL_RENDERBUFFER_OES, 4, GL_DEPTH_COMPONENT16_OES, width , height);
		glFramebufferRenderbufferOES(GL_FRAMEBUFFER_OES, GL_DEPTH_ATTACHMENT_OES, GL_RENDERBUFFER_OES, msaaDepthBuffer);
#endif
	}
	else
	{
		// For this sample, we also need a depth buffer, so we'll create and attach one via another renderbuffer.
		glGenRenderbuffersOES(1, &depthRenderbuffer);
		glBindRenderbufferOES(GL_RENDERBUFFER_OES, depthRenderbuffer);
		glRenderbufferStorageOES(GL_RENDERBUFFER_OES, GL_DEPTH_COMPONENT16_OES, width, height);
		glFramebufferRenderbufferOES(GL_FRAMEBUFFER_OES, GL_DEPTH_ATTACHMENT_OES, GL_RENDERBUFFER_OES, depthRenderbuffer);
	}

	if(glCheckFramebufferStatusOES(GL_FRAMEBUFFER_OES) != GL_FRAMEBUFFER_COMPLETE_OES)
	{
		NSLog(@"failed to make complete framebuffer object %x", glCheckFramebufferStatusOES(GL_FRAMEBUFFER_OES));
		return false;
	}
*/	
	return true;
}
	
void Hardware::destroy_framebuffer()
{
/*
	if(msaaFramebuffer)
		glDeleteFramebuffersOES(1, &msaaFramebuffer);
	msaaFramebuffer = 0;

	if(msaaRenderBuffer)
		glDeleteRenderbuffersOES(1, &msaaRenderBuffer);
	msaaRenderBuffer = 0;

	if(msaaDepthBuffer)
		glDeleteRenderbuffersOES(1, &msaaDepthBuffer);
	msaaDepthBuffer = 0;
	
	if(viewFramebuffer)
		glDeleteFramebuffersOES(1, &viewFramebuffer);
	viewFramebuffer = 0;

	if(viewRenderbuffer)
		glDeleteRenderbuffersOES(1, &viewRenderbuffer);
	viewRenderbuffer = 0;
		
	if(depthRenderbuffer)
		glDeleteRenderbuffersOES(1, &depthRenderbuffer);
	depthRenderbuffer = 0;
*/
}

hard::Texture * Hardware::create_texture(int mipmap_levels, const pictureformat::Format & desired_format, int width, int height)
{
	pictureformat::Format format = desired_format;

#if defined(IPHONE_VERSION) || defined(SAMSUNG_VERSION)
	if(desired_format != pictureformat::PVR4 && desired_format != pictureformat::PVR4A && desired_format != pictureformat::PVR2 && desired_format != pictureformat::PVR2A)
#endif
		format = pictureformat::R8G8B8A8;

	GLint max_texture_size = 0;
	glGetIntegerv( GL_MAX_TEXTURE_SIZE, &max_texture_size );
	return new Texture( this, mipmap_levels, format, width, height, max_texture_size );
}

hard::VertexBuffer * Hardware::create_vertex_buffer( bool dynamic )
{
	return new VertexBuffer( this, dynamic );
}
	
hard::IndexBuffer * Hardware::create_index_buffer()
{
	return new IndexBuffer();
}
	
hard::PointBuffer * Hardware::create_point_buffer()
{
	return new PointBuffer();
}

bool Hardware::start_frame()
{
	if(window->height != height || window->width != width)
		resize_view();

//	[EAGLContext setCurrentContext:(EAGLContext *)context];
	return true;
}

void Hardware::flip()
{
//	glFlush();
/*
	if(multisampleSupported)
	{
#ifndef IPAD_VERSION //3.2 does not have multisampling yet
		// Apple (and the khronos group) encourages you to discard depth
		// render buffer contents whenever is possible
		GLenum attachments[] = {GL_COLOR_ATTACHMENT0_OES, GL_DEPTH_ATTACHMENT_OES};
		glDiscardFramebufferEXT(GL_READ_FRAMEBUFFER_APPLE, 2, attachments);
		
		//Bind both MSAA and View FrameBuffers.
		glBindFramebufferOES(GL_READ_FRAMEBUFFER_APPLE, msaaFramebuffer);
		glBindFramebufferOES(GL_DRAW_FRAMEBUFFER_APPLE, viewFramebuffer);
		
		// Call a resolve to combine both buffers
		glResolveMultisampleFramebufferAPPLE();
#endif
	}
*/
	// Present final image to screen
//	glBindRenderbufferOES(GL_RENDERBUFFER_OES, viewRenderbuffer);
//	[(EAGLContext *)context presentRenderbuffer:GL_RENDERBUFFER_OES];
}

bool Hardware::start_render( const RenderList & render_list, RenderTarget * render_target )
{
	Viewport vp = render_list.viewport;
	
	// Make sure that you are drawing to the current context
//	[EAGLContext setCurrentContext:(EAGLContext *)context];
/*	
	if(multisampleSupported)
		glBindFramebufferOES(GL_FRAMEBUFFER_OES, msaaFramebuffer);
	else
		glBindFramebufferOES(GL_FRAMEBUFFER_OES, viewFramebuffer);
*/
	int ri = height * (1 - vp.left)/2;
	int le = height * (1 - vp.right)/2;
	int bo = width * (1 + vp.top )/2;
	int to = width * (1 + vp.bottom)/2;

	glViewport( to, le, bo - to, ri - le);


	glDepthMask( true );
	if( render_list.clear_render_target )
	{
		glClearColor( 0, 0, 0, 0 );
		glClearDepthf( 1 );
		glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
	}
	else
	{
		glClearDepthf( 1 );
		glClear( GL_DEPTH_BUFFER_BIT );
	}

	glShadeModel( GL_SMOOTH );
	glEnable( GL_LIGHTING );
	glEnable( GL_CULL_FACE );
	glEnable( GL_DEPTH_TEST );
	glEnable( GL_TEXTURE_2D );
	//glEnable( GL_COLOR_MATERIAL );

	glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR ); 

#if defined(SAMSUNG_VERSION) || defined(ANDROID_NDK)
	glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
#else
	glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR );
#endif

	set_blend_mode(BM_ALPHA);
	glDisable( GL_BLEND );
	
	glTexEnvi( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE );

	glEnableClientState( GL_VERTEX_ARRAY );
	glEnableClientState( GL_TEXTURE_COORD_ARRAY );

	return true;
}

static void set_lights( RenderList * render_list )
{
	GLint max_lights = 8;
	glGetIntegerv( GL_MAX_LIGHTS, &max_lights );

	int i = 0;
	RenderLights::const_iterator lit = render_list->lights.begin();
	for( ; lit != render_list->lights.end(); ++lit )
	{
		int light_number = GL_LIGHT0 + i;
		
		GLfloat ambient[] = { 0, 0, 0, 1 };
		glLightfv( light_number, GL_AMBIENT, ambient );
		glLightfv( light_number, GL_SPECULAR, ambient );
		
		if( !lit->is_directional )
		{
			GLfloat position[] = { lit->world_position.x, lit->world_position.y, lit->world_position.z, 1 };
			glLightfv( light_number, GL_POSITION, position );
			
			GLfloat diffuse[] = { lit->diffuse.r, lit->diffuse.g, lit->diffuse.b, 1 };
			glLightfv( light_number, GL_DIFFUSE, diffuse );
			
			glLightf( light_number, GL_CONSTANT_ATTENUATION, lit->clq.x/lit->intensity );
			glLightf( light_number, GL_LINEAR_ATTENUATION, lit->clq.y/lit->intensity );
			glLightf( light_number, GL_QUADRATIC_ATTENUATION, lit->clq.z/lit->intensity );
		}
		else
		{
#if TANKOBOX
			GLfloat direction[] = { lit->world_direction.x, -lit->world_direction.y, -lit->world_direction.z, 0 };
#else
			GLfloat direction[] = { lit->world_direction.x, lit->world_direction.y, lit->world_direction.z, 0 };
#endif
			glLightfv( light_number, GL_POSITION, direction );
			
			GLfloat diffuse[] = { lit->diffuse.r * lit->intensity, lit->diffuse.g * lit->intensity, lit->diffuse.b * lit->intensity, 1 };
			glLightfv( light_number, GL_DIFFUSE, diffuse );
		}
		
		glEnable( light_number );
		++i;
		if( i >= max_lights )
			break;
	}
	
	while( i < max_lights )
	{
		int light_number = GL_LIGHT0 + i;
		glDisable( light_number );
		++i;
	}
}

static void ren_model( const RenderModel & render_model )
{
	if( render_model.ib )
		render_model.vb->render( render_model.ib, render_model.primitive_start, render_model.primitive_count, render_model.vertex_start, render_model.vertex_count );
	else
		render_model.vb->render( render_model.primitive_start, render_model.primitive_count );
}

static void ren_model( const RenderSprite & render_model )
{
	render_model.vb->render( render_model.primitive_start, render_model.primitive_count );
}

static void con_tex_ren_model_diffuse_reflecion_emissive( const RenderList & render_list, const RenderModel & render_model )
{
//	TextureBase * ref = render_model.material.cubic_reflection_texture.texture;
	TextureBase * dif = render_model.material.diffuse_texture.texture;
	TextureBase * emi = render_model.material.emissive_texture.texture;
	
	//simplest style, no multitexturing
	if( !emi )
	{
		if( dif )
			dif->set_in_hardware( 0 );
		else
			Texture::disable(0);

		ren_model( render_model );
	}
	else
	{
		emi->set_in_hardware( 0 );

		GLfloat diffuse[] = { render_model.material.emissive.r,
							  render_model.material.emissive.g,
							  render_model.material.emissive.b,
							  1.0f - render_model.material.transparency };
		
		glMaterialfv( GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, diffuse );
		glMaterialfv( GL_FRONT_AND_BACK, GL_EMISSION, diffuse );
		
		ren_model( render_model );
	}
}

static void con_tex_ren_model_diffuse_reflecion_emissive( const RenderList & render_list, const RenderSprite & render_model )
{
//	TextureBase * ref = render_model.material.cubic_reflection_texture.texture;
	TextureBase * dif = render_model.material.diffuse_texture.texture;
	TextureBase * emi = render_model.material.emissive_texture.texture;

	//simplest style, no multitexturing
	if( !emi )
	{
		if( dif )
			dif->set_in_hardware( 0 );
		else
			Texture::disable(0);

		ren_model( render_model );
	}
	else
	{
		emi->set_in_hardware( 0 );
		
		GLfloat diffuse[] = { render_model.material.emissive.r,
							  render_model.material.emissive.g,
							  render_model.material.emissive.b,
							  1.0f - render_model.material.transparency };
		
		glMaterialfv( GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, diffuse );
		glMaterialfv( GL_FRONT_AND_BACK, GL_EMISSION, diffuse );
		
		ren_model( render_model );
	}
}

static void prepare_model( const Matrix & world, const RenderMaterial & material, bool is_opaque )
{
	glPopMatrix();
	glPushMatrix();
	MatrixGL mat(world);
	mat.glMult();

	//see directx driver for details
	GLfloat diffuse[] = { material.emissive.r,
						  material.emissive.g,
                          material.emissive.b,
						  1 - material.transparency };

	glMaterialfv( GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, diffuse );
	glMaterialfv( GL_FRONT_AND_BACK, GL_EMISSION, diffuse );
}

static void set_z_less_write()
{
	glDepthFunc( GL_LESS );
	glDepthMask( true );
}

static void set_z_lessequal_nowrite()
{
	glDepthFunc( GL_LEQUAL );
	glDepthMask( false );
}

static void set_alpha_test_keep_only_1()
{
	glAlphaFunc( GL_EQUAL, 0xFF );
	glEnable( GL_ALPHA_TEST );
}

static void set_alpha_test_off()
{
	glDisable( GL_ALPHA_TEST );
}

void Hardware::render( RenderList * rl, RenderTarget * render_target )
{
	const RenderList & render_list = *rl;

	if( !start_render( render_list, render_target ) )
		return;

	rl->calculate( 0, 0 );

	glMatrixMode( GL_PROJECTION );
	glLoadIdentity();
	
	MatrixGL mat(rl->camera_projection);
	mat.glMult();
	
	attenuation_point_sprites[0] = 0;
	attenuation_point_sprites[1] = 0;
	attenuation_point_sprites[2] = 0;

	//calculate attenuation for point sprites, test for perspective projection camera
	if(rl->camera_projection.rc(3,2) != 0)
	{
	float H = width;
	double h = 2.0 / rl->camera_projection.rc(1,1);
	double D0 = sqrt(2.0)*H/h;
	double k = 1.0/(1.0 + 2.0 * (1/rl->camera_projection.rc(1,1))*(1/rl->camera_projection.rc(1,1)));
	attenuation_point_sprites[2] = (float)(1.0/(D0*D0)*k);
	}
	else
	{
		attenuation_point_sprites[0] = 0.0001;
	}
	
	//no point sprites on Android Hero
#if !defined(ANDROID_NDK)
	glPointParameterfv( GL_POINT_DISTANCE_ATTENUATION, attenuation_point_sprites );
#endif
	
//	glMultMatrixf( (float *)&rl->camera_projection );
//	MatrixGL( rl->camera_projection ).save();

	glMatrixMode( GL_MODELVIEW );
	mat = MatrixGL(rl->view_mat);
	mat.glLoad();
	set_lights( rl );
	
	GLfloat global_ambient[] = { rl->ambient_color.r, rl->ambient_color.g, rl->ambient_color.b, 1 };
	glLightModelfv( GL_LIGHT_MODEL_AMBIENT, global_ambient );
	
	glPushMatrix();
	
	glCullFace( GL_FRONT );
	set_z_less_write();
///	set_alpha_test_keep_only_1();
	{
		RenderOrder::const_iterator cit = render_list.render_order.begin();
		for( ; cit != render_list.render_order.begin() + render_list.first_transparent_model; ++cit )
		{
			const RenderModel * model = &render_list.models[cit->number];
			
			// setup blend mode
			set_blend_mode(model->blendMode);
			
			// render
			prepare_model( model->world, model->material, true );
			con_tex_ren_model_diffuse_reflecion_emissive( render_list, *model );
		}
	}
	
	glEnable( GL_BLEND );
	
///	set_alpha_test_off();
	set_z_lessequal_nowrite();
	{
		RenderOrder::const_iterator cit = render_list.render_order.begin() + render_list.first_transparent_model;
		for( ; cit != render_list.render_order.end(); ++cit )
		{
			if(cit->is_sprite)
			{
				const RenderSprite * sprite = &render_list.sprites[cit->number];
				
				// setup blend mode
				//set_blend_mode(sprite->blendMode);
				
				// render
				prepare_model( sprite->world, sprite->material, false );
				con_tex_ren_model_diffuse_reflecion_emissive( render_list, *sprite );
			}
			else
			{
				const RenderModel * model = &render_list.models[cit->number];
				
				// setup blend mode
				set_blend_mode(model->blendMode);
				
				// render
				prepare_model( model->world, model->material, false );
				con_tex_ren_model_diffuse_reflecion_emissive( render_list, *model );
			}
		}
	}

	glPopMatrix();
	
//	Texture::report_statistics();
//	VertexBuffer::report_statistics();
}

}
}
}
}
